/** @file vbo.cpp
	@brief VertexBufferObjects */

#include "vbo.h"

using namespace G3;

//==============================================================================
// Constructors
//==============================================================================
G3::VBO::VBO () {
    // Initialize ids
    for (int i=0; i<5; i++) {
        m_buffer_ids [i] = -1;
        m_buffer_lookup [i] = -1;
    }

    // Vertex and index buffers only by default
    m_num_buffers = 2;
    // Mark the VBO as uninitialized
    m_initialized = false;

    m_vertices = NULL;
    m_texcoords = NULL;
    m_normals = NULL;
    m_tangents = NULL;
    m_indexes = NULL;
    m_materials = NULL;

    m_num_vertices = 0;
    m_num_indexes = 0;
    m_num_materials = 0;

    // Let user specify the amount of memory reserved
    m_reserved_memory = 0;

    m_material_applied = false;
    m_current_material = 0;
}

//==============================================================================
// Destructors
//==============================================================================
G3::VBO::~VBO () {
    clear ();
}

//==============================================================================
// Binds the VBO to current rendering
//==============================================================================
bool G3::VBO::bind () {
    // VBO needs to be initialized beforehand
    if (!m_initialized) {
        if (log)
            log->Report ("VBO::bind: VBO must be initialized before use..\n");
        return false;
    }

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::bind: GL_ARB_vertex_buffer_object not supported..\n");
        return false;
    }

    // Vertex buffer
    glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_VERTEX_BUF]]);
    glVertexPointer (3, GL_FLOAT, 0, 0);
    glEnableClientState (GL_VERTEX_ARRAY);

    // Index buffer
    glBindBufferARB (GL_ELEMENT_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_INDEX_BUF]]);
    glEnableClientState (GL_INDEX_ARRAY);

    if (m_buffer_lookup [VBO_TEXCOORD_BUF] != -1) {
        glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_TEXCOORD_BUF]]);
        glTexCoordPointer (2, GL_FLOAT, 0, 0);
        glEnableClientState (GL_TEXTURE_COORD_ARRAY);
    }
    if (m_buffer_lookup [VBO_NORMAL_BUF] != -1) {
        glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_NORMAL_BUF]]);
        glNormalPointer (GL_FLOAT, 0, 0);
        glEnableClientState (GL_NORMAL_ARRAY);
    }

    return true;
}

//==============================================================================
// Unbinds the VBO from current rendering
//==============================================================================
bool G3::VBO::unbind () {
    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::unbind: GL_ARB_vertex_buffer_object not supported..\n");
        return false;
    }

    glDisableClientState (GL_NORMAL_ARRAY);
    glDisableClientState (GL_TEXTURE_COORD_ARRAY);
    glDisableClientState (GL_INDEX_ARRAY);
    glDisableClientState (GL_VERTEX_ARRAY);

    glBindBufferARB (GL_ARRAY_BUFFER_ARB, 0);
    glBindBufferARB (GL_ELEMENT_ARRAY_BUFFER_ARB, 0);

    return true;
}

//==============================================================================
// Binds a material
//==============================================================================
bool G3::VBO::bind_material (int i_mat_id) {
    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::bind_material: GL_ARB_vertex_buffer_object not supported..\n");
        return false;
    }

    if (i_mat_id < 0) {
        m_material_applied = false;
        return true;
    }

    if (m_materials == NULL) {
        if (log)
            log->Report ("VBO::bind_material: There are no materials in this VBO\n");
        return false;
    }
    if (i_mat_id >= (int) m_num_materials) {
        if (log)
            log->Report ("VBO::bind_material: There are only %d materials in this VBO. However, material nr. %d was asked\n", m_num_materials, i_mat_id);
        return false;
    }

    if (!m_material_applied || i_mat_id != m_current_material) {
        m_material_applied = true;
        m_current_material = i_mat_id;

        m_materials [i_mat_id].glApply ();
    }

    return true;
}

//==============================================================================
// Unbinds the last bound material
//==============================================================================
bool G3::VBO::unbind_material () {
    if (!m_material_applied)
        return false;

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::unbind_material: GL_ARB_vertex_buffer_object not supported..\n");
        return false;
    }

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::unbind_material: GL_ARB_vertex_buffer_object not supported..\n");
        return false;
    }

    Assert (m_materials, "Impossible but true: There are no materials, but one of them has been bound");
    Assert (m_current_material < m_num_materials, "Impossible but true: A nonexistent material has been bound");

    if (m_material_applied) {
        m_materials [m_current_material].glUnApply ();

        m_material_applied = false;
        m_current_material = 0;
    }

    return true;
}

//==============================================================================
// Allocates memory for the VBO on the graphics card
//==============================================================================
void G3::VBO::alloc (int i_num_verts, bool b_buf_static) {
    // No vertices to allocate memory for?
    if (i_num_verts < 1) {
        if (log)
            log->Report ("VBO::alloc: Nothing to allocate memory for (number of vertices is %d)..\n", i_num_verts);
        return;
    }

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::alloc: GL_ARB_vertex_buffer_object not supported..\n");
        return;
    }

    int error = 0;
    GLenum usage;

    if (b_buf_static)
        usage = GL_STATIC_DRAW_ARB;
    else
        usage = GL_DYNAMIC_DRAW_ARB;

    try {
        // Vertices
        glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_VERTEX_BUF]]);
        glBufferDataARB (GL_ARRAY_BUFFER_ARB, i_num_verts * sizeof (G3::VEC3F), NULL, usage);

        error = glGetError ();
        if (error != GL_NO_ERROR)
            throw "vertex buffer";

        // Indexes
        glBindBufferARB (GL_ELEMENT_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_INDEX_BUF]]);
        glBufferDataARB (GL_ELEMENT_ARRAY_BUFFER_ARB, i_num_verts * sizeof (int), NULL, usage);

        error = glGetError ();
        if (error != GL_NO_ERROR)
            throw "index buffer";

        // Texcoords
        if (m_buffer_lookup [VBO_TEXCOORD_BUF] != -1) {
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_TEXCOORD_BUF]]);
            glBufferDataARB (GL_ARRAY_BUFFER_ARB, i_num_verts * sizeof (G3::VEC2F), NULL, usage);

            error = glGetError ();
            if (error != GL_NO_ERROR)
                throw "texcoords buffer";
        }

        // Normals
        if (m_buffer_lookup [VBO_NORMAL_BUF] != -1) {
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_NORMAL_BUF]]);
            glBufferDataARB (GL_ARRAY_BUFFER_ARB, i_num_verts * sizeof (G3::VEC3F), NULL, usage);

            error = glGetError ();
            if (error != GL_NO_ERROR)
                throw "normal buffer";
        }
    } catch (const char *str) {
        if (log)
            log->Report ("VBO::alloc: %s: OpenGL threw error %d..\n", str, error);
    }
}

//==============================================================================
// Rebuilds the VBO
//==============================================================================
void G3::VBO::build () {
    if (!m_initialized) {
        if (log)
            log->Report ("VBO::build: VBO must be initialized before use..\n");
        return;
    }

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::build: GL_ARB_vertex_buffer_object not supported..\n");
        return;
    }

    int error = 0;

    try {
        // Vertices
        glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_VERTEX_BUF]]);
        glBufferSubDataARB (GL_ARRAY_BUFFER_ARB, 0, m_num_vertices * sizeof (G3::VEC3F), m_vertices);

        error = glGetError ();
        if (error != GL_NO_ERROR)
            throw "vertex buffer";

        // Indexes
        glBindBufferARB (GL_ELEMENT_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_INDEX_BUF]]);
        glBufferSubDataARB (GL_ELEMENT_ARRAY_BUFFER_ARB, 0, m_num_indexes * sizeof (int), m_indexes);

        error = glGetError ();
        if (error != GL_NO_ERROR)
            throw "index buffer";

        // Texcoords
        if (m_buffer_lookup [VBO_TEXCOORD_BUF] != -1) {
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_TEXCOORD_BUF]]);
            glBufferSubDataARB (GL_ARRAY_BUFFER_ARB, 0, m_num_vertices * sizeof (G3::VEC2F), m_texcoords);

            error = glGetError ();
            if (error != GL_NO_ERROR)
                throw "texcoords buffer";
        }

        // Normals
        if (m_buffer_lookup [VBO_NORMAL_BUF] != -1) {
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_NORMAL_BUF]]);
            glBufferSubDataARB (GL_ARRAY_BUFFER_ARB, 0, m_num_vertices * sizeof (G3::VEC3F), m_normals);

            error = glGetError ();
            if (error != GL_NO_ERROR)
                throw "normal buffer";
        }
    } catch (const char *str) {
        if (log)
            log->Report ("VBO::build: %s: OpenGL threw error %d..\n", str, error);
    }
}

//==============================================================================
// Initializes the VBO buffers
//==============================================================================
void G3::VBO::init (uint i_nv, G3::VEC3F *p_v, uint i_nid, uint *p_id, uint i_nm, G3::MATERIAL *p_m, G3::VEC2F *p_uv, G3::VEC3F *p_n, G3::VEC3F *p_t) {
    m_num_vertices = i_nv;
    m_num_indexes = i_nid;
    m_num_materials = i_nm;

    m_vertices = p_v;
    m_indexes = p_id;
    m_materials = p_m;
    m_texcoords = p_uv;
    m_normals = p_n;

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::init: GL_ARB_vertex_buffer_object not supported..\n");
        return;
    }

    // Are the pointers valid?
    if (m_vertices == NULL && m_indexes != NULL) {
        if (log != NULL)
            log->Report ("VBO::init: At least vertices and indexes are needed to define a buffer\n");
        return;
    }

    m_buffer_lookup [VBO_VERTEX_BUF] = 0;
    m_buffer_lookup [VBO_INDEX_BUF] = 1;

    if (m_texcoords != NULL) {
        m_buffer_lookup [VBO_TEXCOORD_BUF] = m_num_buffers;
        m_num_buffers++;
    }
    if (m_normals != NULL) {
        m_buffer_lookup [VBO_NORMAL_BUF] = m_num_buffers;
        m_num_buffers++;
    }

    int error = 0;

    if (log)
        log->Report ("VBO::init: Generating %d buffers..\n", m_num_buffers);

    glGenBuffersARB (m_num_buffers, (GLuint *) &m_buffer_ids [0]);

    error = glGetError ();
    if (error != GL_NO_ERROR) {
        if (log)
            log->Report ("VBO::init: OpenGL threw error %d (%s)..\n", error, gluErrorString (error));
    }

    // Allocate the reserved_memory for this VBO
    int a = sizeof (G3::VEC3F);

    if (m_indexes != NULL)
        a += sizeof (int);
    if (m_texcoords != NULL)
        a += sizeof (G3::VEC2F);
    if (m_normals != NULL)
        a += sizeof (G3::VEC3F);

    int nverts = G3_MAX ((int) m_reserved_memory / a, m_num_vertices);
    m_reserved_memory = nverts * a;

    if (log)
        log->Report ("This VBO has reserved %d bytes of memory for %d vertices..\n", m_reserved_memory, nverts);

    // Allocate the memory
    alloc (nverts);

    m_initialized = true;
}

//==============================================================================
// Updates a region of the VBO
//==============================================================================
void G3::VBO::update (BUF_ID e_id, int i_offset, int i_size, void *p_data) {
    if (p_data == NULL) {
        if (log)
            log->Report ("VBO::update: Data pointer argument is NULL..\n");
        return;
    }

    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::update: GL_ARB_vertex_buffer_object not supported..\n");
        return;
    }

    if (m_buffer_lookup [e_id] == -1) {
        if (log)
            log->Report ("VBO::update: VBO is not initialized for buffer no. %d..\n", e_id);
        return;
    }

    switch (e_id) {
        case VBO_VERTEX_BUF:
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_VERTEX_BUF]]);
            glBufferSubDataARB (GL_ARRAY_BUFFER_ARB, i_offset, i_size, p_data);
            break;

        case VBO_INDEX_BUF:
            glBindBufferARB (GL_ELEMENT_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_INDEX_BUF]]);
            glBufferSubDataARB (GL_ELEMENT_ARRAY_BUFFER_ARB, i_offset, i_size, p_data);
            break;

        case VBO_TEXCOORD_BUF:
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_TEXCOORD_BUF]]);
            glBufferSubDataARB (GL_ARRAY_BUFFER_ARB, i_offset, i_size, p_data);
            break;

        case VBO_NORMAL_BUF:
            glBindBufferARB (GL_ARRAY_BUFFER_ARB, m_buffer_ids [m_buffer_lookup [VBO_NORMAL_BUF]]);
            glBufferSubDataARB (GL_ARRAY_BUFFER_ARB, i_offset, i_size, p_data);
            break;

        default: break;
    }

    int error = glGetError ();
    if (error != GL_NO_ERROR) {
        if (log)
            log->Report ("VBO::update: OpenGL threw error %d (%s)..\n", error, gluErrorString (error));
    }
}

//==============================================================================
// Dumps the VBO into the log
//==============================================================================
void G3::VBO::dump() {
    if (log) {
        log->Report( "Number of vertices: %d\n", m_num_vertices );
        log->Report( "Number of indexes: %d\n", m_num_indexes );
        log->Report( "Number of materials: %d\n", m_num_materials );
        log->Report( "Memory reserved: %d\n", m_reserved_memory );
        log->Report( "Has the VBO been initialized: %s\n", G3_BOOLEAN( m_initialized ) );

        for (int i=0; i<5; i++) {
            if (m_buffer_lookup [i] != -1)
                log->Report( "Buffer no. %d has id %d\n", i, m_buffer_ids [m_buffer_lookup [i]] );
        }
    }
}

//==============================================================================
// Clears the VBO buffers
//==============================================================================
void G3::VBO::clear () {
    // Are VBO's supported?
    if (!GLEE_ARB_vertex_buffer_object) {
        if (log)
            log->Report ("VBO::clear: GL_ARB_vertex_buffer_object not supported..\n");
        return;
    }

    glDeleteBuffersARB (m_num_buffers, (GLuint *) &m_buffer_ids [0]);

    int error = glGetError ();
    if (error != GL_NO_ERROR) {
        if (log)
            log->Report ("VBO::clear: OpenGL threw error %d (%s)..\n", error, gluErrorString (error));
    }
}
